%%% Copyright 2010 Google Inc.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.

%%% @copyright 2010 Google Inc. All Rights Reserved.
%%% @author <jacoby@google.com> Jacoby Thwaites [http://sites/madcloud]
%%% @version {@version}
%%% @doc
%%% Tests view_fsm module.
%%% @headerfile "../include/txm.hrl"

-module(view_fsm_tests).

%% Include eunit and view headers
-include_lib("eunit/include/eunit.hrl").
-include("txm.hrl").
-include("process.hrl").
-include("client.hrl").

%% Useful for shell-based testing.
-export([start_fixture/0, stop_fixture/0]).
-export([handle_input/4]).
-export([listener/1]).

%% Test group with setup and teardown of prog tree.
view_test_() ->

  % Tests depend on preceding tests.
  {inorder,

   % This tuple specifies a setup fun(), teardown fun() and list of test fun().
   {setup,

    % Setup starts dependent servers and returns a view for use in the
    % individual tests.
    % @spec fun() -> pid()
    fun() ->
         start_fixture()
    end,

    % Cleanup stops the view and the mock server. Don't stop the process
    % supervisor, it's not stoppable.
    fun(_) ->
         stop_fixture()
    end,

    % List of functions returning lists of tests.
    [basic_tests(),
     client_data_tests(),
     single_notification_tests()]}}.

%% @doc Starts the fixtures required for testing view_fsm.
start_fixture() ->
  prog_srv:start_link(prog_mock),
  cache_srv:start_link(?PROCESS_CACHE),
  mnesia:start(),
  view_db:create_tables(),
  view_db:start_link().

%% @doc Stops the fixtures required for testing view_fsm.
stop_fixture() ->
  view_db:stop(),
  mnesia:stop(),
  cache_srv:stop(?PROCESS_CACHE),
  prog_srv:stop().

%% Basic tests start, attach, detach and stop a view_fsm.
basic_tests() ->
  {setup,

   % Setup.
   fun() ->
        {ok, V} = view_fsm:start_link(?MODULE),
        V
   end,

   % Teardown.
   fun(V) ->
        view_fsm:stop(V)
   end,

   % Tests.
   fun(V) ->
        [
          % Test view start and attach to folder.
          fun() ->
               Spec = #?TXM_ATTACH_SPEC{prog = "/mock/Primes"},
               view_fsm:attach(V, Spec),
               ?assertEqual(state_attached, view_fsm:current_state(V))
          end,

          % Test detach from folder.
          fun() ->
               ?assertEqual(ok, view_fsm:detach(V)),
               ?assertEqual(state_idle, view_fsm:current_state(V))
          end
          ]
   end}.

%% Client data tests ensure client data is stored and returned correctly.
client_data_tests() ->
  [
    fun() ->
         {ok, V} = view_fsm:start_link(?MODULE, {foo, bar}),
         ?assertEqual({foo, bar}, view_fsm:get_client_data(V))
    end
  ].

%% Single notification tests start a process. The listener updates its
%% state record and returns it to the test function for asserting correctness.
-record(counts, {
                  replyTo :: pid(),
                  firstDivisor = 0,
                  test = 0,
                  iterate = 0,
                  showPrime = 0,
                  showNotPrime = 0}).

single_notification_tests() ->
  {setup,

   % Setup starts 2 views and attaches them to the root and backend folders
   % of the mock prog.
   fun() ->
        {ok, V1} = view_fsm:start_link(?MODULE),
        Spec1 = #?TXM_ATTACH_SPEC{prog = "/mock/Primes"},
        view_fsm:attach(V1, Spec1),

        {ok, V2} = view_fsm:start_link(?MODULE),
        Spec2 = Spec1#?TXM_ATTACH_SPEC{folder = "backend"},
        view_fsm:attach(V2, Spec2),
        {V1, V2}
   end,

   % Teardown detaches and stops the views.
   fun({V1, V2}) ->
        view_fsm:detach(V1),
        view_fsm:detach(V2),
        view_fsm:stop(V1),
        view_fsm:stop(V2)
   end,

   % Tests.
   fun({V1, V2}) ->
        [
          % Fire a notification event, leave plenty of time for the test
          % to complete.
          {timeout, 300,
           fun() ->

                % Start the process that counts op invocations and store
                % it in the views' client data.
                Listener = spawn_link(?MODULE, listener,
                                      [#counts{replyTo = self()}]),
                view_fsm:set_client_data(V1, Listener),
                view_fsm:set_client_data(V2, Listener),

                % Notification that starts the TXM process.
                Result = view_fsm:notify(V1, "Start", "Start", [{"n", 17}]),
                ?assertEqual(ok, Result),

                % Wait for counted results.
                receive
                  {_Listener, _Ref, Counts} ->
                    ok
                  end,

                % Check the returned op invocation counts are correct.
                ?assertEqual(1, Counts#counts.firstDivisor),
                ?assertEqual(2, Counts#counts.test),
                ?assertEqual(1, Counts#counts.iterate),
                ?assertEqual(0, Counts#counts.showNotPrime),
                ?assertEqual(1, Counts#counts.showPrime)
           end
            }
          ]
   end}.

%% Listener process counts callbacks on each op.
listener(#counts{replyTo = ReplyTo,
                 firstDivisor = FirstDivisor,
                 test = Test,
                 iterate = Iterate,
                 showPrime = ShowPrime,
                 showNotPrime = ShowNotPrime} = Counts) ->
  receive
    {_Pid, _Ref, {count, OpName}} ->
      case OpName of
        "FirstDivisor" ->
          % Count and loop.
          listener(Counts#counts{firstDivisor = FirstDivisor + 1});
        "Test" ->
          % Count and loop.
          listener(Counts#counts{test = Test + 1});
        "Iterate" ->
          % Count and loop.
          listener(Counts#counts{iterate = Iterate + 1});
        "ShowPrime" ->
          % Count and stop.
          ReplyTo ! {self(), make_ref(), Counts#counts{showPrime = ShowPrime + 1}};
        "ShowNotPrime" ->
          % Count and stop.
          ReplyTo ! {self(), make_ref(), Counts#counts{showNotPrime = ShowNotPrime + 1}}
      end
    end.

%% ----------------------------------------------------------------------------
%% Client callbacks.
%% ----------------------------------------------------------------------------
handle_input(V2, Ref, "FirstDivisor" = Op, _Fields) ->
  count(V2, Op),
  view_fsm:reply(V2, Ref, "Divisor", [{"div", 3}]);

handle_input(V2, Ref, "Test" = Op, Fields) ->
  count(V2, Op),
  N = proplists:get_value("n", Fields),
  D = proplists:get_value("div", Fields),

  case {D > math:sqrt(N), N rem D} of
    {true, _} -> view_fsm:reply(V2, Ref, "Yes", [{"YES", true}]);
    {false, 0} -> view_fsm:reply(V2, Ref, "No", [{"NO", true}]);
    {false, _} -> view_fsm:reply(V2, Ref, "Maybe", [{"MAYBE", true}])
  end;

handle_input(V1, _Ref, "ShowNotPrime" = Op, _Fields) ->
  count(V1, Op);

handle_input(V1, _Ref, "ShowPrime" = Op, _Fields) ->
  count(V1, Op);

handle_input(V2, Ref, "Iterate" = Op, Fields) ->
  count(V2, Op),
  N = proplists:get_value("n", Fields),
  D = proplists:get_value("div", Fields),
  view_fsm:reply(V2, Ref, "Iterate", [{"n", N}, {"div", D + 2}]).

% Counts the specified op.
count(View, Op) ->
  Listener = view_fsm:get_client_data(View),
  Listener ! {self(), make_ref(), {count, Op}}.
